package com.data.hijack.util;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.AbstractSelectableChannel;
import java.util.*;
import java.util.zip.GZIPOutputStream;

import org.apache.log4j.Logger;
import org.jsoup.Connection;
import org.jsoup.Jsoup;

import com.data.hijack.constant.Constants;
import com.data.hijack.http.HttpSession;
import com.data.hijack.http.Method;
import com.data.hijack.service.SysConfig;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigValue;

/**
 *
 * @author husq
 */
public final class AppUtils {
    private static final Logger logger = Logger.getLogger(AppUtils.class);

    private AppUtils() {
        throw new RuntimeException("no");
    }

    /**
     * 
     * @param list
     * @param elemSize
     * @return
     */
    public static List<List<?>> splitList(final List<?> list, final int elemSize) {
        if (null == list) {
            return null;
        }

        return new ArrayList<List<?>>(1024) {
            private static final long serialVersionUID = 2145783230458836577L;

            {
                int size = list.size();
                if (logger.isDebugEnabled()) {
                    logger.debug("The total number of elements in the list:  " + size);
                }

                int group = (size % elemSize == 0) ? size / elemSize : size / elemSize + 1;
                for (int num = 1; num <= group; num++) {
                    int end = elemSize * num;
                    int begin = end - elemSize;
                    if (end > size) {
                        end = size;
                    }
                    add(list.subList(begin, end));
                }
            }
        };
    }

    /**
     * 
     * @param path
     * @return
     * @throws FileNotFoundException
     */
    public static Map<String, Object> LoadJson(File path) throws FileNotFoundException {
        InputStreamReader isr = new InputStreamReader(new FileInputStream(path));
        Config config = ConfigFactory.parseReader(isr);

        return parseJson(config);
    }

    /**
     * 
     * @param json
     * @return
     */
    public static Map<String, Object> parseJson(String json) {
        Config config = ConfigFactory.parseString(json);
        if (config == null || config.isEmpty()) {
            return new HashMap();
        }

        return parseJson(config);
    }

    /**
     * 
     * @param config
     * @return
     */
    public static Map<String, Object> parseJson(Config config) {
        Map<String, Object> result = new HashMap(8);
        Set<Map.Entry<String, ConfigValue>> set = config.entrySet();
        Iterator<Map.Entry<String, ConfigValue>> itera = set.iterator();
        while (itera.hasNext()) {
            Map.Entry<String, ConfigValue> me = itera.next();
            String key = me.getKey();
            ConfigValue value = me.getValue();
            if (value != null) {
                Object obj = value.unwrapped();
                if (obj instanceof String) {
                    result.put(key, value.unwrapped());
                } else {
                    Object o = value.unwrapped();
                    if (o instanceof List) {
                        result.put(key, o);
                    }
                }
            }
        }

        return result;
    }

    public static void safeClose(Object closeable) {
        try {
            if (closeable != null) {
                if (closeable instanceof Closeable) {
                    ((Closeable)closeable).close();
                } else if (closeable instanceof Socket) {
                    ((Socket)closeable).close();
                } else if (closeable instanceof ServerSocket) {
                    ((ServerSocket)closeable).close();
                } else {
                    throw new IllegalArgumentException("Unknown object to close");
                }
            }
        } catch (IOException e) {
            logger.error("Could not close", e);
        } catch (Exception e) {
            logger.error("Could not close", e);
        }
    }

    public static void closeChannel(AbstractSelectableChannel channel, Selector selector) {
        safeClose(selector);
        safeClose(channel);
    }

    /**
     * 
     * @param bizName
     * @param consts
     * @param config
     * @return
     */
    public static String getValue(String bizName, String consts, Map<String, String> config) {
        String bnKey = bizName + Constants.DOT + consts;
        String value = null;
        try {
            value = config.get(bnKey);
        } catch (Exception e) {
            logger.error(bnKey, e);
            value = null;
        }

        if (StringUtils.isNotBlank(value)) {
            value = value.trim();
        }

        return value;
    }

    public static HttpSession getHttpSession(SocketChannel socketChannel, String charset, String tmpDir)
        throws Exception {
        HttpSession hs = new HttpSession(tmpDir);
        hs.setCharset(charset);
        hs.setChannel(socketChannel);
        int ok = hs.preHandle();
        if (ok == -1) {
            return null;
        }

        return hs;
    }

    /**
     * 
     * @param msg
     * @param consts
     * @param hs
     * @param mpForm
     * @param mpFile
     * @return
     * @throws IOException
     */
    public static Connection.Response sendByHttp(String msg, SysConfig consts, HttpSession hs,
        Map<String, List<String>> mpForm, Map<String, byte[]> mpFile) throws UnsupportedEncodingException {
        String pt = hs.getUri();
        String port = "";
        if (StringUtils.isNotBlank(port)) {
            port = ":" + consts.getPort();
        }
        String url = consts.getSchema().toLowerCase() + "://" + consts.getHost() + port;
        String quryStr = "";
        String queryData = getQueryString(mpForm, msg);
        if (Method.GET.equals(hs.getMethod())) {
            quryStr = hs.getQueryParameterString();
            if (consts.getIsInterceptable().equalsIgnoreCase(Constants.IS_INTERCEPTABLE_0)) {
                quryStr = queryData;
            }
        }

        if (StringUtils.isNotBlank(quryStr) && !quryStr.startsWith("?")) {
            quryStr = "?" + quryStr;
        } else {
            quryStr = "";
        }
        if (Objects.equals(consts.getIsChanged(), Constants.REMOTE_IS_CHANGED_1)) {
            String path = consts.getPath();
            if (StringUtils.isNotBlank(path) && !path.startsWith("/")) {
                path = "/" + path;
            }
            path = StringUtils.isBlank(path) ? "" : path;
            url += path + quryStr;
        } else if (Objects.equals(consts.getIsChanged(), Constants.REMOTE_IS_CHANGED_0)) {
            url += pt + quryStr;
        } else {
            Asserts.check(StringUtils.isNotBlank(consts.getIsChanged()), "Configuration error");
        }

        int tm = Integer.parseInt(consts.getTimeout());
        Method m = hs.getMethod();

        Connection connection = Jsoup.connect(url).timeout(tm).maxBodySize(0);
        if (Method.POST.equals(m)) {
            connection.method(Connection.Method.POST);
            if (hs.getContentType().isMultipart()) {
                mpForm.forEach((mk, mv) -> {
                    mv.forEach((lv) -> {
                        connection.data(mk, lv);
                    });
                });

                mpFile.forEach((k, v) -> {
                    String filename = mpForm.get(k).get(0);
                    ByteArrayInputStream bis = new ByteArrayInputStream(v);
                    connection.data(k, filename, bis);
                });
            } else {
                String body = queryData;
                connection.headers(hs.getHeaders()).requestBody(body);
            }
        } else if (Method.GET.equals(m)) {
            connection.method(Connection.Method.GET).headers(hs.getHeaders());
        } else {
            Asserts.check(false, "Request has encountered an error.");
        }

        connection.ignoreHttpErrors(true).ignoreContentType(true);
        try {
            return connection.execute();
        } catch (IOException e) {
            logger.error(TextUtils.format(e, "%s[%s]", "Connection error", url));
            throw new RuntimeException("Connection server error");
        }
    }

    private static String getQueryString(Map<String, List<String>> mpForm, String dmsg) {
        if (Objects.isNull(mpForm) || mpForm.isEmpty()) {
            return dmsg;
        }

        StringBuilder mesg = new StringBuilder(32);
        mpForm.forEach((k, v) -> {
            mesg.append(k).append("=").append(v).append("&");
        });
        if (mesg.length() > 0) {
            mesg.deleteCharAt(mesg.length() - 1);
        }

        return mesg.toString();
    }

    public static void writeRespByHttp(SocketChannel socketChannel, Connection.Response response, String charset)
        throws IOException {
        Map<String, String> headers = response.headers();
        String httpVer = "HTTP/1.1";
        String statusMsg = response.statusMessage();
        int status = response.statusCode();
        String sp = " ";
        String protoFirstLine = httpVer + sp + status + sp + statusMsg + "\r\n";

        StringBuilder header = new StringBuilder(protoFirstLine);
        Iterator<Map.Entry<String, String>> itera = headers.entrySet().iterator();
        boolean gzip = false;
        while (itera.hasNext()) {
            Map.Entry<String, String> me = itera.next();
            String key = me.getKey();
            if ("Content-Encoding".equalsIgnoreCase(key)) {
                header.append(getContentEncoding(key));
                gzip = true;
                continue;
            }

            header.append(key).append(": ").append(me.getValue()).append("\r\n");
        }
        header.append("\r\n");

        socketChannel.write(ByteBuffer.wrap(header.toString().getBytes(charset)));
        ByteBuffer body = ByteBuffer.wrap(response.bufferUp().bodyAsBytes());
        if (gzip) {
            body = ByteBuffer.wrap(gzip(response.bufferUp().bodyAsBytes()));
        }
        if (!writeChunk(body, headers, charset, socketChannel)) {
            socketChannel.write(body);
        }

        if (logger.isDebugEnabled()) {
            logger.debug(header.toString() + response.bufferUp().body());
        }
    }

    private static String getContentEncoding(String key) {
        String str = key + ": " + "gzip" + "\r\n";
        return str;
    }

    private static byte[] gzip(byte[] b) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        GZIPOutputStream gzip = new GZIPOutputStream(out);
        gzip.write(b);

        return out.toByteArray();
    }

    private static boolean writeChunk(ByteBuffer body, Map<String, String> headers, String charset,
        SocketChannel channel) throws IOException {
        String te = headers.get("Transfer-Encoding");
        if (StringUtils.isBlank(te)) {
            te = headers.get("transfer-encoding");
        }
        if (StringUtils.isNotBlank(te) && "chunked".equalsIgnoreCase(te)) {
            int chunkSize = 8192;
            while (body.hasRemaining()) {
                int length = Math.min(body.remaining(), chunkSize);
                byte[] chunk = new byte[length];
                body.get(chunk, 0, length);

                write(chunk, length, charset, channel);
            }
            finish(channel, charset);

            return true;
        }

        return false;
    }

    private static void write(byte[] b, int len, String charset, SocketChannel out) throws IOException {
        if (len == 0) {
            return;
        }

        out.write(ByteBuffer.wrap(String.format("%x\r\n", len).getBytes(charset)));
        out.write(ByteBuffer.wrap(b));
        out.write(ByteBuffer.wrap("\r\n".getBytes(charset)));
    }

    private static void finish(SocketChannel out, String charset) throws IOException {
        out.write(ByteBuffer.wrap("0\r\n\r\n".getBytes(charset)));
    }
}
